package csel.model;

import java.util.LinkedList;
import java.util.Dictionary;
import java.util.Enumeration;
import java.util.Hashtable;

public class Entity extends GameObject implements Ticked, java.io.Serializable
{
	java.util.LinkedList<Observer> observers = new LinkedList<Observer>();
	private static final long serialVersionUID = 1L;
	Tile location;
	Occupation occupation;
	private final Inventory inventory;
	private Hashtable<String, EquippableItem> equippedItems;
	private String name;
	private GameGlobals globals; // don't delete this! for future use. -- david
	
	/* Stats */
	private int lives;
	private int strength;
	private int agility;
	private int intellect;
	private int hardiness;
	private int experience;
	private int speed;
	private int lifeLeft;// damage
	

	Tile teleportTile;

	private boolean canClimb;
	private boolean canSwim;

	private volatile Direction movingDirection;
	private int ticksSinceLastMove = speed;

	public Entity(GameGlobals globals, Occupation occupation)
	{
		equippedItems = new Hashtable<String, EquippableItem>();
		inventory = new Inventory();
		this.occupation = occupation;
		this.globals = globals;

		occupation.setInitialStats(this);
		lifeLeft = 0;
	}

	public void tick()
	{
		ticksSinceLastMove++;
		if (movingDirection != null)
		{
			moveOne(movingDirection);
		}
		if(teleportTile!= null)
		{
			teleport(teleportTile);
		}
	}

	public boolean move(Tile nextTile)
	{
		Tile last = location;
		if (nextTile.enter(this))
		{
			System.out.println("Moved from: "+location.getLocation().getX() +", " + location.getLocation().getY());
			location.leave(this);
			location = nextTile;
			System.out.println("Moved to: "+location.getLocation().getX() +", " + location.getLocation().getY());
			notifyO();
			return (true);
		} else
		{
			return (false);
		}
	}

	public boolean move(Direction movementDirection)
	{
		movingDirection = movementDirection;
		direction = movementDirection;
		return (moveOne(movementDirection));
	}

	public boolean moveOne(Direction movementDirection)
	{
		// get correct tile and call move(Tile)
		if (ticksSinceLastMove >= speed)
		{
			Tile nextTile = location.getNeighbor(movementDirection);
			if (nextTile != null)
			{
				switch(movementDirection)
				{
					case NORTHEAST:
						//NOTE objectImmovablePresent does not check to see if entity is present
						if(ObjectImmovablePresent(location.getNeighbor(Direction.NORTH)) && 
								ObjectImmovablePresent(location.getNeighbor(Direction.EAST)))
						{
								return false;
						}
						break;
						
					case NORTHWEST:
						//NOTE objectImmovablePresent does not check to see if entity is present
						if(ObjectImmovablePresent(location.getNeighbor(Direction.NORTH)) && 
								ObjectImmovablePresent(location.getNeighbor(Direction.WEST)))
						{
							return false;
						}
						break;
						
					case SOUTHWEST:
						//NOTE objectImmovablePresent does not check to see if entity is present
						if(ObjectImmovablePresent(location.getNeighbor(Direction.SOUTH)) && 
								ObjectImmovablePresent(location.getNeighbor(Direction.WEST)))
						{
							return false;
						}
						break;
					
					case SOUTHEAST:
						//NOTE objectImmovablePresent does not check to see if entity is present
						if(ObjectImmovablePresent(location.getNeighbor(Direction.SOUTH)) && 
								ObjectImmovablePresent(location.getNeighbor(Direction.EAST)))
						{
							return false;
						}
						break;
					
					default:
						break;
				}
				
				ticksSinceLastMove = 0;
				return (move(nextTile));
			} else
			{
				return (false);
			}
		}
		return (false);
	}
	
	//does not include entities
	private boolean ObjectImmovablePresent(Tile tile)
	{
		if(tile == null)
		{
			return true;
		}
		if(tile.getTerrain().canPass(this))
		{
			if(tile.getItem() != null && tile.getItem().isImpassable())
			{
				return true;
			}
			return false;
		}
		return true;
	}

	public boolean stop()
	{
		movingDirection = null;
		return (true);
	}

	public boolean canClimb()
	{
		return (canClimb);
	}

	public boolean canSwim()
	{
		return (canSwim);
	}

	public Tile getLocation()
	{
		return location;
	}

	public void setLocation(Tile location)
	{
		this.location = location;
		notifyO();
	}

	public Inventory getInventory()
	{
		return inventory;
	}

	@SuppressWarnings("unchecked")
	public Dictionary<String, EquippableItem> getEquippedItems()
	{
		return (Hashtable<String, EquippableItem>) equippedItems.clone();
	}

	public void setEquippedItem(EquippableItem i)
	{
		EquippableItem oldItem = equippedItems.get(i.getGoesOn());
		if (oldItem != null)
		{
			inventory.addEquippableItem(oldItem);
		}
		equippedItems.put(i.getGoesOn(), i);
		inventory.removeItem(i);
		notifyO();
	}

	public void unequipItem(EquippableItem i)
	{
		inventory.addEquippableItem(equippedItems.remove(i.getGoesOn()));
		notifyO();
	}

	public String getName()
	{
		return name;
	}

	public void setName(String name)
	{
		this.name = name;
		notifyO();
	}

	public int getStrength()
	{
		return strength;
	}

	public void setStrength(int strength)
	{
		this.strength = strength;
		notifyO();
	}

	public int getAgility()
	{
		return agility;
	}

	public void setAgility(int agility)
	{
		this.agility = agility;
		notifyO();
	}

	public int getIntellect()
	{
		return intellect;
	}

	public void setIntellect(int intellect)
	{
		this.intellect = intellect;
		notifyO();
	}

	public int getHardiness()
	{
		return hardiness;
	}

	public void setHardiness(int hardiness)
	{
		this.hardiness = hardiness;
		notifyO();
	}

	public int getExperience()
	{
		return experience;
	}

	public void setExperience(int experience)
	{
		this.experience = experience;
		notifyO();
	}

	public int getSpeed()
	{
		return speed;
	}

	public void setSpeed(int speed)
	{
		this.speed = speed;
		notifyO();
	}

	public int getLife()
	{
		/* main structure should be implemented */
		return (getLevel() * hardiness)-lifeLeft;
	}

	public int getLevel()
	{
		/* main structure should be implemented */
		return ((experience / 1000) + 1);
	}

	public int getMana()
	{
		/* main structure should be implemented */
		return (2 * intellect * getLevel()) + getLevel();
	}

	public int getOffense()
	{
		int offense = getLevel() * getStrength();
		Enumeration<String> items = equippedItems.keys();
		String item;
		while (items.hasMoreElements())
		{
			item = items.nextElement();
			offense = equippedItems.get(item).applyOffenseBonus(offense);
		}
		return offense; // + equipped weapon
	}

	public int getDefense()
	{
		/* main structure should be implemented */
		return (agility * getLevel());
	}

	public int getArmor()
	{
		int armor = hardiness;
		Enumeration<String> items = equippedItems.keys();
		String item;
		while (items.hasMoreElements())
		{
			item = items.nextElement();
			armor = equippedItems.get(item).applyArmorBonus(armor);
		}
		return armor; // +get equipped armor
	}

	public void setCanClimb(boolean canClimb)
	{
		this.canClimb = canClimb;
	}

	public void setCanSwim(boolean canSwim)
	{
		this.canSwim = canSwim;
	}

	public int getLives()
	{
		return lives;
	}

	public void setLives(int lives)
	{
		this.lives = lives;
		if (lives <= 0)
		{
			// TODO: STOP CHRONO TRIGGER HERE.
			
			ticksSinceLastMove = 0;
			this.lives = 0;
		} 
		else
		{
			if(location!=null)
			{
				teleportTile = location.getMap().getTile(5, 4);
			}
		}
		notifyO();
	}
	
	public boolean teleport(Tile t)
	{
		if(ticksSinceLastMove >= speed-1)
		{
			move(t);
			teleportTile = null;
			movingDirection = null;
		}
		else
		{
			teleportTile = t;
		}
		return true;
	}
	public int getLifeLeft()
	{
		return lifeLeft;
	}

	public void setLifeLeft(int lifeLeft)
	{
		this.lifeLeft = lifeLeft;
		
		if(getLife() <= 0)
		{
			setLives(lives-1);
			if(lives > 0)
				this.lifeLeft = 0;
			else
				this.lifeLeft = getLife() + this.lifeLeft;
		}
		if (this.lifeLeft < 0)
		{
			this.lifeLeft = 0;
		}
		notifyO();
	}
	public Occupation getOccupation()
	{
		return(occupation);
	}
	public void setTicksSinceLastMove(int val)
	{
		this.ticksSinceLastMove = val;
		notifyO();
	}
	public void addObserver(Observer o)
	{
		observers.add(o);
	}
	public boolean removeObserver(Observer o)
	{
		return(observers.remove(o));
	}
	public void notifyO()
	{
		for(Observer o: observers)
		{
			o.modelUpdate(this);
		}
		inventory.clearHasChanged();
	}
	
	public String toString()
	{
		return String.format("%s", getName());
	}
	
	/**
	 * Custom impelementation of writeObject() to avoid serializing Observers.
	 */
	@SuppressWarnings("unchecked")
	private void writeObject(java.io.ObjectOutputStream out) throws java.io.IOException
	{
		java.util.LinkedList<Observer> list;
		
		System.out.println("Calling Entity.writeObject().");
		
		/* Temporarily clear Observers. */
		list = (java.util.LinkedList<Observer>)(observers.clone());
		observers.clear();
		
		/* Write it. */
		out.defaultWriteObject();
		
		/* Restore Observers. */
		observers = list;
	}
}
